﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Diagnostics;
using System.Drawing;

namespace System.Windows.Forms
{
    partial class ErrorProvider
    {
        /// <summary>
        ///  There is one ControlItem for each control that the ErrorProvider is tracking state for.
        ///  It contains the values of all the extender properties.
        /// </summary>
        internal partial class ControlItem
        {
            private static readonly int s_accessibilityProperty = PropertyStore.CreateKey();

            private string _error;
            private readonly Control _control;
            private ErrorWindow _window;
            private readonly ErrorProvider _provider;
            private int _iconPadding;
            private ErrorIconAlignment _iconAlignment;
            private const int _startingBlinkPhase = 10; // We want to blink 5 times

            /// <summary>
            ///  Construct the item with its associated control, provider, and a unique ID. The ID is
            ///  used for the tooltip ID.
            /// </summary>
            public ControlItem(ErrorProvider provider, Control control, IntPtr id)
            {
                _iconAlignment = DefaultIconAlignment;
                _error = string.Empty;
                Id = id;
                _control = control;
                _provider = provider;
                _control.HandleCreated += new EventHandler(OnCreateHandle);
                _control.HandleDestroyed += new EventHandler(OnDestroyHandle);
                _control.LocationChanged += new EventHandler(OnBoundsChanged);
                _control.SizeChanged += new EventHandler(OnBoundsChanged);
                _control.VisibleChanged += new EventHandler(OnParentVisibleChanged);
                _control.ParentChanged += new EventHandler(OnParentVisibleChanged);
                Properties = new PropertyStore();
            }

            /// <summary>
            ///  The Accessibility Object for this ErrorProvider
            /// </summary>
            internal AccessibleObject AccessibilityObject
            {
                get
                {
                    AccessibleObject accessibleObject = (AccessibleObject)Properties.GetObject(s_accessibilityProperty);

                    if (accessibleObject is null)
                    {
                        accessibleObject = CreateAccessibilityInstance();
                        Properties.SetObject(s_accessibilityProperty, accessibleObject);
                    }

                    return accessibleObject;
                }
            }

            /// <summary>
            ///  Constructs the new instance of the accessibility object for this ErrorProvider. Subclasses
            ///  should not call base.CreateAccessibilityObject.
            /// </summary>
            private AccessibleObject CreateAccessibilityInstance()
            {
                return new ControlItemAccessibleObject(this, _window, _control.ParentInternal, _provider);
            }

            public void Dispose()
            {
                if (_control != null)
                {
                    _control.HandleCreated -= new EventHandler(OnCreateHandle);
                    _control.HandleDestroyed -= new EventHandler(OnDestroyHandle);
                    _control.LocationChanged -= new EventHandler(OnBoundsChanged);
                    _control.SizeChanged -= new EventHandler(OnBoundsChanged);
                    _control.VisibleChanged -= new EventHandler(OnParentVisibleChanged);
                    _control.ParentChanged -= new EventHandler(OnParentVisibleChanged);
                }

                _error = string.Empty;
            }

            /// <summary>
            ///  Returns the unique ID for this control. The ID used as the tooltip ID.
            /// </summary>
            public IntPtr Id { get; }

            /// <summary>
            ///  Returns or set the phase of blinking that this control is currently
            ///  in.  If zero, the control is not blinking. If odd, then the control
            ///  is blinking, but invisible. If even, the control is blinking and
            ///  currently visible. Each time the blink timer fires, this value is
            ///  reduced by one (until zero), thus causing the error icon to appear
            ///  or disappear.
            /// </summary>
            public int BlinkPhase { get; set; }

            /// <summary>
            ///  Returns or sets the icon padding for the control.
            /// </summary>
            public int IconPadding
            {
                get => _iconPadding;
                set
                {
                    if (_iconPadding == value)
                    {
                        return;
                    }

                    _iconPadding = value;
                    UpdateWindow();
                }
            }

            /// <summary>
            ///  Returns or sets the error description string for the control.
            /// </summary>
            public string Error
            {
                get => _error;
                set
                {
                    if (value is null)
                    {
                        value = string.Empty;
                    }

                    // If the error is the same and the blinkStyle is not AlwaysBlink, then
                    // we should not add the error and not start blinking.
                    if (_error.Equals(value) && _provider.BlinkStyle != ErrorBlinkStyle.AlwaysBlink)
                    {
                        return;
                    }

                    bool adding = _error.Length == 0;
                    _error = value;
                    if (value.Length == 0)
                    {
                        RemoveFromWindow();
                    }
                    else
                    {
                        if (adding)
                        {
                            AddToWindow();
                        }
                        else
                        {
                            if (_provider.BlinkStyle != ErrorBlinkStyle.NeverBlink)
                            {
                                StartBlinking();
                            }
                            else
                            {
                                UpdateWindow();
                            }
                        }
                    }
                }
            }

            /// <summary>
            ///  Returns or sets the location of the error icon for the control.
            /// </summary>
            public ErrorIconAlignment IconAlignment
            {
                get => _iconAlignment;
                set
                {
                    SourceGenerated.EnumValidator.Validate(value);

                    if (_iconAlignment == value)
                    {
                        return;
                    }

                    _iconAlignment = value;
                    UpdateWindow();
                }
            }

            /// <summary>
            ///  Returns true if the tooltip for this control item is currently shown.
            /// </summary>
            public bool ToolTipShown { get; set; }

            internal ErrorIconAlignment RTLTranslateIconAlignment(ErrorIconAlignment align)
            {
                if (_provider.RightToLeft)
                {
                    switch (align)
                    {
                        case ErrorIconAlignment.TopLeft:
                            return ErrorIconAlignment.TopRight;
                        case ErrorIconAlignment.MiddleLeft:
                            return ErrorIconAlignment.MiddleRight;
                        case ErrorIconAlignment.BottomLeft:
                            return ErrorIconAlignment.BottomRight;
                        case ErrorIconAlignment.TopRight:
                            return ErrorIconAlignment.TopLeft;
                        case ErrorIconAlignment.MiddleRight:
                            return ErrorIconAlignment.MiddleLeft;
                        case ErrorIconAlignment.BottomRight:
                            return ErrorIconAlignment.BottomLeft;
                        default:
                            Debug.Fail("Unknown ErrorIconAlignment value");
                            return align;
                    }
                }
                else
                {
                    return align;
                }
            }

            /// <summary>
            ///  Returns the location of the icon in the same coordinate system as the control being
            ///  extended. The size passed in is the size of the icon.
            /// </summary>
            internal Rectangle GetIconBounds(Size size)
            {
                int x = 0;
                int y = 0;

                switch (RTLTranslateIconAlignment(IconAlignment))
                {
                    case ErrorIconAlignment.TopLeft:
                    case ErrorIconAlignment.MiddleLeft:
                    case ErrorIconAlignment.BottomLeft:
                        x = _control.Left - size.Width - _iconPadding;
                        break;
                    case ErrorIconAlignment.TopRight:
                    case ErrorIconAlignment.MiddleRight:
                    case ErrorIconAlignment.BottomRight:
                        x = _control.Right + _iconPadding;
                        break;
                }

                switch (IconAlignment)
                {
                    case ErrorIconAlignment.TopLeft:
                    case ErrorIconAlignment.TopRight:
                        y = _control.Top;
                        break;
                    case ErrorIconAlignment.MiddleLeft:
                    case ErrorIconAlignment.MiddleRight:
                        y = _control.Top + (_control.Height - size.Height) / 2;
                        break;
                    case ErrorIconAlignment.BottomLeft:
                    case ErrorIconAlignment.BottomRight:
                        y = _control.Bottom - size.Height;
                        break;
                }

                return new Rectangle(x, y, size.Width, size.Height);
            }

            /// <summary>
            ///  If this control's error icon has been added to the error window, then update the
            ///  window state because some property has changed.
            /// </summary>
            private void UpdateWindow() => _window?.Update(timerCaused: false);

            /// <summary>
            ///  If this control's error icon has been added to the error window, then start blinking
            ///  the error window.
            /// </summary>
            private void StartBlinking()
            {
                if (_window != null)
                {
                    BlinkPhase = _startingBlinkPhase;
                    _window.StartBlinking();
                }
            }

            /// <summary>
            ///  Add this control's error icon to the error window.
            /// </summary>
            private void AddToWindow()
            {
                // if we are recreating the control, then add the control.
                if (_window is null &&
                    (_control.Created || _control.RecreatingHandle) &&
                    _control.Visible && _control.ParentInternal != null &&
                    _error.Length > 0)
                {
                    _window = _provider.EnsureErrorWindow(_control.ParentInternal);
                    _window.Add(this);
                    // Make sure that we blink if the style is set to AlwaysBlink or BlinkIfDifferentError
                    if (_provider.BlinkStyle != ErrorBlinkStyle.NeverBlink)
                    {
                        StartBlinking();
                    }
                }
            }

            /// <summary>
            ///  Remove this control's error icon from the error window.
            /// </summary>
            private void RemoveFromWindow()
            {
                if (_window != null)
                {
                    _window.Remove(this);
                    _window = null;
                }
            }

            /// <summary>
            ///  This is called when a property on the control is changed.
            /// </summary>
            private void OnBoundsChanged(object sender, EventArgs e) => UpdateWindow();

            void OnParentVisibleChanged(object sender, EventArgs e)
            {
                BlinkPhase = 0;
                RemoveFromWindow();
                AddToWindow();
            }

            /// <summary>
            ///  Retrieves our internal property storage object. If you have a property
            ///  whose value is not always set, you should store it in here to save
            ///  space.
            /// </summary>
            private PropertyStore Properties { get; }

            /// <summary>
            ///  This is called when the control's handle is created.
            /// </summary>
            private void OnCreateHandle(object sender, EventArgs e) => AddToWindow();

            /// <summary>
            ///  This is called when the control's handle is destroyed.
            /// </summary>
            private void OnDestroyHandle(object sender, EventArgs e) => RemoveFromWindow();
        }
    }
}
